---
title: HM - 鸿蒙-知乎评论
---

# 鸿蒙-2. 知乎评论

## 阶段案例-知乎回复

    <img src="./images/26.png" width="240" />


### 1. 底部输入区域

- 抽离 `Nav` `Comment` 组件
- 使用 `Stack` 组件底部输入框固定在下方
- 加上 `Scroll` 将来页面内容可以滚动

```typescript
@Entry
@Component
struct Index {
  build() {
    Stack({ alignContent: Alignment.Bottom }) {
      Column() {
        Scroll() {
          Column() {
            // 导航
            NavComp()
            // 评论
            CommentComp()
            // 分割线
            Divider()
              .strokeWidth(8)
              .color('#f5f5f5')
            // 回复列表
            
          }
          .padding({ bottom: 50 })
        }
      }
      .width('100%')
      .height('100%')

      Row({ space: 15 }){
        TextInput({ placeholder: '回复~' })
          .layoutWeight(1)
        Text('发布')
          .fontColor('#069')
      }
      .padding({ left: 15, right: 15 })
      .width('100%')
      .height(50)
      .backgroundColor('#fff')
      .border({ width: { top: 0.5 }, color: '#e4e4e4' })
    }

  }
}

// 导航
@Component
struct NavComp {
  build() {
    Row() {
      Row() {
        Image($r('app.media.ic_public_arrow_left'))
          .width(16)
          .aspectRatio(1)
        // svg 图标可以使用填充颜色
        // .fillColor('red')
      }
      .width(24)
      .aspectRatio(1)
      .backgroundColor('#f5f5f5')
      .borderRadius(12)
      .justifyContent(FlexAlign.Center)
      .margin({ left: 16 })

      Text('评论回复')
        .layoutWeight(1)
        .textAlign(TextAlign.Center)
        .padding({ right: 40 })
    }
    .height(40)
    .border({ width: { bottom: 0.5 }, color: '#e4e4e4' })
  }
}

// 评论
@Component
struct CommentComp {
  build() {
    Row() {
      Image($r('app.media.avatar'))
        .width(32)
        .aspectRatio(1)
        .borderRadius(16)
      Column({ space: 5 }) {
        Text('周杰伦')
          .width('100%')
          .fontWeight(FontWeight.Bold)
          .fontSize(15)
        Text('大理石能雕刻出肌肉和皮肤的质感，那个年代的工匠好牛啊')
          .width('100%')
        Row() {
          Text('10-21 · IP属地北京')
            .fontSize(12)
            .fontColor('#c3c4c5')
          Row({ space: 4 }) {
            Image($r('app.media.ic_public_heart'))
              .width(14)
              .aspectRatio(1)
              .fillColor('#c3c4c5')
            Text('100')
              .fontSize(12)
              .fontColor('#c3c4c5')
          }
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
      }
      .layoutWeight(1)
      .padding({ left: 10 })
    }
    .padding(15)
    .alignItems(VerticalAlign.Top)
  }
}
```


### 2. 静态回复列表

- 参考评论组件，使用 `ForEach` 循环相同的回复容器

```typescript
Column() {
  Text('回复 100')
    .width('100%')
    .fontWeight(600)
  ForEach([1, 2, 3, 4, 5, 6, 7], () => {
    Row() {
      Image($r('app.media.avatar'))
        .width(32)
        .aspectRatio(1)
        .borderRadius(16)
      Column({ space: 5 }) {
        Text('周杰伦')
          .width('100%')
          .fontWeight(FontWeight.Bold)
          .fontSize(15)
        Text('大理石能雕刻出肌肉和皮肤的质感，那个年代的工匠好牛啊')
          .width('100%')
        Row() {
          Text('10-21 · IP属地北京')
            .fontSize(12)
            .fontColor('#c3c4c5')
          Row({ space: 4 }) {
            Image($r('app.media.ic_public_heart'))
              .width(14)
              .aspectRatio(1)
              .fillColor('#c3c4c5')
            Text('100')
              .fontSize(12)
              .fontColor('#c3c4c5')
          }
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
      }
      .layoutWeight(1)
      .padding({ left: 10 })
    }
    .padding({ top: 15, bottom: 15 })
    .alignItems(VerticalAlign.Top)
  })

}
.padding(15)
```

### 3. 实现渲染

- 使用 `class` 定义好回复数据模型 `ReplyItem`
- 初始化一些模拟数据
- 完成页面渲染

```typescript title="models/index.ets"
export class ReplyItem {
  id: number
  avatar: string
  author: string
  content: string
  time: string
  area: string
  likeNum: number
  likeFlag?: boolean
}

export const replyList: ReplyItem[] = [
  {
    id: 1,
    avatar: 'https://picx.zhimg.com/027729d02bdf060e24973c3726fea9da_l.jpg?source=06d4cd63',
    author: '偏执狂-妄想家',
    content: '更何况还分到一个摩洛哥[惊喜]',
    time: '11-30',
    area: '海南',
    likeNum: 34
  },
  {
    id: 2,
    avatar: 'https://pic1.zhimg.com/v2-5a3f5190369ae59c12bee33abfe0c5cc_xl.jpg?source=32738c0c',
    author: 'William',
    content: '当年希腊可是把1：0发挥到极致了',
    time: '11-29',
    area: '北京',
    likeNum: 58
  },
  {
    id: 3,
    avatar: 'https://picx.zhimg.com/v2-e6f4605c16e4378572a96dad7eaaf2b0_l.jpg?source=06d4cd63',
    author: 'Andy Garcia',
    content: '欧洲杯其实16队球队打正赛已经差不多，24队打正赛意味着正赛阶段在小组赛一样有弱队。',
    time: '11-28',
    area: '上海',
    likeNum: 10
  },
  {
    id: 4,
    avatar: 'https://picx.zhimg.com/v2-53e7cf84228e26f419d924c2bf8d5d70_l.jpg?source=06d4cd63',
    author: '正宗好鱼头',
    content: '确实眼红啊，亚洲就没这种球队，让中国队刷',
    time: '11-27',
    area: '香港',
    likeNum: 139
  },
  {
    id: 5,
    avatar: 'https://pic1.zhimg.com/v2-eeddfaae049df2a407ff37540894c8ce_l.jpg?source=06d4cd63',
    author: '柱子哥',
    content: '我是支持扩大的，亚洲杯欧洲杯扩到32队，世界杯扩到64队才是好的，世界上有超过200支队伍，欧洲区55支队伍，亚洲区47支队伍，即使如此也就六成出现率',
    time: '11-27',
    area: '旧金山',
    likeNum: 29
  },
  {
    id: 6,
    avatar: 'https://picx.zhimg.com/v2-fab3da929232ae911e92bf8137d11f3a_l.jpg?source=06d4cd63',
    author: '飞轩逸',
    content: '禁止欧洲杯扩军之前，应该先禁止世界杯扩军，或者至少把亚洲名额一半给欧洲。',
    time: '11-26',
    area: '里约',
    likeNum: 100
  }
]
```

```typescript title="pages/Index.ets"
import { ReplyItem, replyList } from '../models'
@Entry
@Component
struct Index {

  @State
  replyList: ReplyItem[] = replyList

  build() {
    Stack({ alignContent: Alignment.Bottom }) {
      Column() {
        Scroll() {
          Column() {
            // 导航
            NavComp()
            // 评论
            CommentComp()
            // 分割线
            Divider()
              .strokeWidth(8)
              .color('#f5f5f5')
            // 回复列表
            Column() {
              Text('回复 100')
                .width('100%')
                .fontWeight(600)
              ForEach(
                this.replyList, 
                (item: ReplyItem) => {
                  Row() {
                    Image(item.avatar)
                      .width(32)
                      .aspectRatio(1)
                      .borderRadius(16)
                    Column({ space: 5 }) {
                      Text(item.author)
                        .width('100%')
                        .fontWeight(FontWeight.Bold)
                        .fontSize(15)
                      Text(item.content)
                        .width('100%')
                      Row() {
                        Text(`${item.time} · IP属地${item.area}`)
                          .fontSize(12)
                          .fontColor('#c3c4c5')
                        Row({ space: 4 }) {
                          Image($r('app.media.ic_public_heart'))
                            .width(14)
                            .aspectRatio(1)
                            .fillColor('#c3c4c5')
                          Text(item.likeNum.toString())
                            .fontSize(12)
                            .fontColor('#c3c4c5')
                        }
                      }
                      .width('100%')
                      .justifyContent(FlexAlign.SpaceBetween)
                    }
                    .layoutWeight(1)
                    .padding({ left: 10 })
                  }
                  .padding({ top: 15, bottom: 15 })
                  .alignItems(VerticalAlign.Top)
                },
                // key 有默认你规则
                // key 为了元素复用
                // 如果没有写，会自动生成一个key，index_ + JSON.stringify(item)，不建议不写
                // (item: ReplyItem) => item.id.toString() 写一个ID做唯一标识，需要key也更新才能更新对应UI
                // item => id + likeNum + likeFlag 把需要更新的字段合在一起当做key
                // ({ id, likeNum, likeFlag }) => JSON.stringify({ id, likeNum, likeFlag })

                // 学习了 @Observed @ObjectLink 这样也可以更新~
                // (item: ReplyItem) => item.id.toString()
              )

            }
            .padding(15)
          }
          .padding({ bottom: 50 })
        }
      }
      .width('100%')
      .height('100%')

      Row({ space: 15 }){
        TextInput({ placeholder: '回复~' })
          .layoutWeight(1)
        Text('发布')
          .fontColor('#069')
      }
      .padding({ left: 15, right: 15 })
      .width('100%')
      .height(50)
      .backgroundColor('#fff')
      .border({ width: { top: 0.5 }, color: '#e4e4e4' })
    }

  }
}
```


### 4. 实现点赞

- 注册点赞区域点击事件
- 通过索引复制的方式完成数据的更新和UI的更新

```typescript
onLike(item: ReplyItem) {
  const reply = { ...item }
  if (reply.likeFlag) {
    reply.likeNum--
    reply.likeFlag = false
    promptAction.showToast({ message: '取消点赞' })
  } else {
    reply.likeNum++
    reply.likeFlag = true
    promptAction.showToast({ message: '点赞成功' })
  }
  const index = this.replyList.findIndex(rep => rep.id === reply.id)
  this.replyList[index] = reply
}
```

```typescript
Row({ space: 4 }) {
  Image($r('app.media.heart'))
    .width(14)
    .height(14)
    .fillColor(item.likeFlag ? '#ff6600' : '#c3c4c5')
    .margin({ right: 4 })
  Text(item.likeNum.toString())
    .fontSize(14)
    .fontColor(item.likeFlag ? '#ff6600' : '#c3c4c5')
}
.onClick(() => {
  this.onLike(item)
})
```


### 6. 进行回复

- 收集输入框数据
- 发布评论内容，和情况输入内容
- 需要扩展头像类型兼容 `Resource` 类型

```diff title="models/Index.ets"
export class ReplyItem {
  id: number
+  avatar: string | Resource
  author: string
  content: string
  time: string
  area: string
  likeNum: number
  likeFlag?: boolean
}
```


```typescript title="pages/Index.ets"
onReply () {
  const reply: ReplyItem = {
    id: Math.random(),
    content: this.content,
    author: 'Zhousg',
    avatar: $r('app.media.avatar'),
    time: '12-01',
    likeNum: 0,
    area: '北京'
  }
  this.replyList.unshift(reply)
  this.content = ''
  promptAction.showToast({ message: '回复成功' })
}
```

```typescript title="pages/Index.ets"
Row({ spcae: 15 }) {
  TextInput({ placeholder: '回复~', text: this.content })
    .placeholderColor('#c3c4c5')
    .layoutWeight(1)
    .onChange((value) => {
      this.content = value
    })
  Text('发布')
    .fontSize(14)
    .fontColor('#09f')
    .onClick(()=>{
      this.onReply()
    })
}
```